/**
 *  Copyright (C) 2015 Topology LP
 *  All rights reserved.
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to
 *  deal in the Software without restriction, including without limitation the
 *  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 *  sell copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 *  IN THE SOFTWARE.
 */

#ifndef CPPCODEC_DETAIL_CODEC
#define CPPCODEC_DETAIL_CODEC

#include <assert.h>
#include <stdint.h>
#include <string>
#include <vector>

#include "access.hpp"
#include "raw_result_buffer.hpp"

namespace cppcodec {
    namespace detail {

        // SFINAE: Templates sometimes beat sensible overloads - make sure we don't call the wrong one.
        template <typename T>
        struct non_numeric : std::enable_if<!std::is_arithmetic<T>::value> { };


        /**
         * Public interface for all the codecs. For API documentation, see README.md.
         */
        template <typename CodecImpl>
        class codec
        {
        public:
            //
            // Encoding

            // Convenient version, returns an std::string.
            static std::string encode(const uint8_t* binary, size_t binary_size);
            static std::string encode(const char* binary, size_t binary_size);
            // static std::string encode(const T& binary); -> provided by template below

            // Convenient version with templated result type.
            template <typename Result> static Result encode(const uint8_t* binary, size_t binary_size);
            template <typename Result> static Result encode(const char* binary, size_t binary_size);
            template <typename Result = std::string, typename T = std::vector<uint8_t>>
            static Result encode(const T& binary);

            // Reused result container version. Resizes encoded_result before writing to it.
            template <typename Result>
            static void encode(Result& encoded_result, const uint8_t* binary, size_t binary_size);
            template <typename Result>
            static void encode(Result& encoded_result, const char* binary, size_t binary_size);
            template <typename Result, typename T, typename non_numeric<T>::type* = nullptr>
            static void encode(Result& encoded_result, const T& binary);

            // Raw pointer output, assumes pre-allocated memory with size > encoded_size(binary_size).
            static size_t encode(
                char* encoded_result, size_t encoded_buffer_size,
                const uint8_t* binary, size_t binary_size) noexcept;
            static size_t encode(
                char* encoded_result, size_t encoded_buffer_size,
                const char* binary, size_t binary_size) noexcept;
            template<typename T>
            static size_t encode(
                char* encoded_result, size_t encoded_buffer_size,
                const T& binary) noexcept;

            // Calculate the exact length of the encoded string based on binary size.
            static constexpr size_t encoded_size(size_t binary_size) noexcept;

            //
            // Decoding

            // Convenient version, returns an std::vector<uint8_t>.
            static std::vector<uint8_t> decode(const char* encoded, size_t encoded_size);
            // static std::vector<uint8_t> decode(const T& encoded); -> provided by template below

            // Convenient version with templated result type.
            template <typename Result> static Result decode(const char* encoded, size_t encoded_size);
            template <typename Result = std::vector<uint8_t>, typename T = std::string>
            static Result decode(const T& encoded);

            // Reused result container version. Resizes binary_result before writing to it.
            template <typename Result>
            static void decode(Result& binary_result, const char* encoded, size_t encoded_size);
            template <typename Result, typename T, typename non_numeric<T>::type* = nullptr>
            static void decode(Result& binary_result, const T& encoded);

            // Raw pointer output, assumes pre-allocated memory with size > decoded_max_size(encoded_size).
            static size_t decode(
                uint8_t* binary_result, size_t binary_buffer_size,
                const char* encoded, size_t encoded_size);
            static size_t decode(
                char* binary_result, size_t binary_buffer_size,
                const char* encoded, size_t encoded_size);
            template<typename T> static size_t decode(
                uint8_t* binary_result, size_t binary_buffer_size, const T& encoded);
            template<typename T> static size_t decode(
                char* binary_result, size_t binary_buffer_size, const T& encoded);

            // Calculate the maximum size of the decoded binary buffer based on the encoded string length.
            static constexpr size_t decoded_max_size(size_t encoded_size) noexcept;
        };


        //
        // Inline definitions of the above functions, using CRTP to call into CodecImpl
        //

        //
        // Encoding

        template <typename CodecImpl>
        inline std::string codec<CodecImpl>::encode(const uint8_t* binary, size_t binary_size)
        {
            return encode<std::string>(binary, binary_size);
        }

        template <typename CodecImpl>
        inline std::string codec<CodecImpl>::encode(const char* binary, size_t binary_size)
        {
            return encode<std::string>(reinterpret_cast<const uint8_t*>(binary), binary_size);
        }

        template <typename CodecImpl>
        template <typename Result>
        inline Result codec<CodecImpl>::encode(const uint8_t* binary, size_t binary_size)
        {
            Result encoded_result;
            encode(encoded_result, binary, binary_size);
            return encoded_result;
        }

        template <typename CodecImpl>
        template <typename Result>
        inline Result codec<CodecImpl>::encode(const char* binary, size_t binary_size)
        {
            return encode<Result>(reinterpret_cast<const uint8_t*>(binary), binary_size);
        }

        template <typename CodecImpl>
        template <typename Result, typename T>
        inline Result codec<CodecImpl>::encode(const T& binary)
        {
            return encode<Result>(data::uchar_data(binary), data::size(binary));
        }

        template <typename CodecImpl>
        template <typename Result>
        inline void codec<CodecImpl>::encode(
            Result& encoded_result, const uint8_t* binary, size_t binary_size)
        {
            // This overload is where we reserve buffer capacity and call into CodecImpl.
            size_t encoded_buffer_size = encoded_size(binary_size);
            auto state = data::create_state(encoded_result, data::specific_t());
            data::init(encoded_result, state, encoded_buffer_size);

            CodecImpl::encode(encoded_result, state, binary, binary_size);
            data::finish(encoded_result, state);
            assert(data::size(encoded_result) == encoded_buffer_size);
        }

        template <typename CodecImpl>
        template <typename Result>
        inline void codec<CodecImpl>::encode(
            Result& encoded_result, const char* binary, size_t binary_size)
        {
            encode(encoded_result, reinterpret_cast<const uint8_t*>(binary), binary_size);
        }

        template <typename CodecImpl>
        template <typename Result, typename T, typename non_numeric<T>::type*>
        inline void codec<CodecImpl>::encode(Result& encoded_result, const T& binary)
        {
            encode(encoded_result, data::uchar_data(binary), data::size(binary));
        }

        template <typename CodecImpl>
        inline size_t codec<CodecImpl>::encode(
            char* encoded_result, size_t encoded_buffer_size,
            const uint8_t* binary, size_t binary_size) noexcept
        {
            // This overload is where we wrap the result pointer & size.
            data::raw_result_buffer encoded(encoded_result, encoded_buffer_size);
            encode(encoded, binary, binary_size);

            size_t encoded_size = data::size(encoded);
            if (encoded_size < encoded_buffer_size) {
                encoded_result[encoded_size] = '\0';
            }
            return encoded_size;
        }

        template <typename CodecImpl>
        inline size_t codec<CodecImpl>::encode(
            char* encoded_result, size_t encoded_buffer_size,
            const char* binary, size_t binary_size) noexcept
        {
            // This overload is where we wrap the result pointer & size.
            return encode(encoded_result, encoded_buffer_size,
                reinterpret_cast<const uint8_t*>(binary), binary_size);
        }

        template <typename CodecImpl>
        template <typename T>
        inline size_t codec<CodecImpl>::encode(
            char* encoded_result, size_t encoded_buffer_size,
            const T& binary) noexcept
        {
            return encode(encoded_result, encoded_buffer_size, data::uchar_data(binary), data::size(binary));
        }

        template <typename CodecImpl>
        inline constexpr size_t codec<CodecImpl>::encoded_size(size_t binary_size) noexcept
        {
            return CodecImpl::encoded_size(binary_size);
        }


        //
        // Decoding

        template <typename CodecImpl>
        inline std::vector<uint8_t> codec<CodecImpl>::decode(const char* encoded, size_t encoded_size)
        {
            return decode<std::vector<uint8_t>>(encoded, encoded_size);
        }

        template <typename CodecImpl>
        template <typename Result>
        inline Result codec<CodecImpl>::decode(const char* encoded, size_t encoded_size)
        {
            Result result;
            decode(result, encoded, encoded_size);
            return result;
        }

        template <typename CodecImpl>
        template <typename Result, typename T>
        inline Result codec<CodecImpl>::decode(const T& encoded)
        {
            return decode<Result>(data::char_data(encoded), data::size(encoded));
        }

        template <typename CodecImpl>
        template <typename Result>
        inline void codec<CodecImpl>::decode(Result& binary_result, const char* encoded, size_t encoded_size)
        {
            // This overload is where we reserve buffer capacity and call into CodecImpl.
            size_t binary_buffer_size = decoded_max_size(encoded_size);
            auto state = data::create_state(binary_result, data::specific_t());
            data::init(binary_result, state, binary_buffer_size);

            CodecImpl::decode(binary_result, state, encoded, encoded_size);
            data::finish(binary_result, state);
            assert(data::size(binary_result) <= binary_buffer_size);
        }


        template <typename CodecImpl>
        template <typename Result, typename T, typename non_numeric<T>::type*>
        inline void codec<CodecImpl>::decode(Result& binary_result, const T& encoded)
        {
            decode(binary_result, data::char_data(encoded), data::size(encoded));
        }

        template <typename CodecImpl>
        inline size_t codec<CodecImpl>::decode(
            uint8_t* binary_result, size_t binary_buffer_size,
            const char* encoded, size_t encoded_size)
        {
            return decode(reinterpret_cast<char*>(binary_result), binary_buffer_size, encoded, encoded_size);
        }

        template <typename CodecImpl>
        inline size_t codec<CodecImpl>::decode(
            char* binary_result, size_t binary_buffer_size,
            const char* encoded, size_t encoded_size)
        {
            // This overload is where we wrap the result pointer & size.
            data::raw_result_buffer binary(binary_result, binary_buffer_size);
            decode(binary, encoded, encoded_size);
            return data::size(binary);
        }

        template <typename CodecImpl>
        template <typename T>
        inline size_t codec<CodecImpl>::decode(
            uint8_t* binary_result, size_t binary_buffer_size, const T& encoded)
        {
            return decode(reinterpret_cast<char*>(binary_result), binary_buffer_size, encoded);
        }

        template <typename CodecImpl>
        template <typename T>
        inline size_t codec<CodecImpl>::decode(char* binary_result, size_t binary_buffer_size, const T& encoded)
        {
            return decode(binary_result, binary_buffer_size, data::char_data(encoded), data::size(encoded));
        }

        template <typename CodecImpl>
        inline constexpr size_t codec<CodecImpl>::decoded_max_size(size_t encoded_size) noexcept
        {
            return CodecImpl::decoded_max_size(encoded_size);
        }


    } // namespace detail
} // namespace cppcodec

#endif